#!/usr/bin/python3
# ******************************************************************************
# Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
# licensed under the Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#     http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
# PURPOSE.
# See the Mulan PSL v2 for more details.
# ******************************************************************************/
import re
import asyncio
from collections import namedtuple

from constant import Constant
from core.platform import CvePlatform
from logger import logger
from util.github_api import Github
from util.gitlab_api import Gitlab
from conf import settings


class Patch:
    """
    Patch acquisition, analysis implementation class
    """

    def __init__(self, cve_num, rpm_name):
        self.cve_num = cve_num
        self.rpm_name = rpm_name
        self.patch_info_list = list()
        self.patch_detail_list = list()
        self.issue_pr_dict = dict()
        self.pr_status_dict = dict()
        self.github_api = Github()
        self.gitlab_api = Gitlab()
        self._SummaryInfo = namedtuple("SummaryInfo", ["pr_list", "issues_list"])
        self._PatchDetail = namedtuple("PatchDetail", ["platform", "details"])

    @property
    def issue_relation_info(self):
        return {"issue": {"url": None, "prs": []}}

    @property
    def pr_relation_info(self):
        return {"url": None, "status": None, "commits": []}

    async def find_patches_detail(self):
        """
        Further analyze the obtained patch information to get the final patch link
        :return: self.patch_detail_list
        """
        await self._find_patches_info()
        # commit,pr,issue summary structure,convenient for deduplication and concurrent search
        summary_info = self._SummaryInfo(pr_list=list(), issues_list=list())
        for patch_info in self.patch_info_list:
            summary_info.pr_list.extend(patch_info.get("pr", []))
            summary_info.issues_list.extend(patch_info.get("issue", []))

        # Find issue linked pr and add it to the pr list obtained earlier
        if summary_info.issues_list:
            issue_task_list = await self._create_async_task(
                self._get_issue_link_pr, summary_info.issues_list
            )
            issue_done_task, _ = await asyncio.wait(issue_task_list)
            for task in issue_done_task:
                summary_info.pr_list.extend(task.result())

        if summary_info.pr_list:
            # Query pr status
            pr_status_task_list = await self._create_async_task(
                self._get_pr_status, summary_info.pr_list
            )
            await asyncio.wait(pr_status_task_list)
            # Find pr contain commits
            pr_task_list = await self._create_async_task(
                self._get_pr_contain_commits, summary_info.pr_list
            )
            await asyncio.wait(pr_task_list)

        # Generate the final information that needs to be output
        self._convert_path_detail()
        logger.info(
            f'Find the patch information of cve "{self.cve_num}" as: \n{self.patch_detail_list}'
        )
        return self.patch_detail_list

    async def _find_patches_info(self):
        """
        Implementation method of preliminary search for CVE patch information
        :return: self.patch_info_list
        """
        logger.info(
            f"Start to obtain patches info of {self.cve_num} for {self.rpm_name}"
        )
        crawler_task = [
            asyncio.create_task(
                CvePlatform(
                    cve_platform,
                    self.cve_num,
                    cve_platform["url"],
                    cve_platform["format"],
                ).crawling_patch()
            )
            for cve_platform in settings.get_platform()
        ]
        done_task, _ = await asyncio.wait(crawler_task)
        self.patch_info_list.extend(
            [task.result() for task in done_task if task.result()]
        )

    @staticmethod
    async def _create_async_task(method, issue_pr_list):
        """
        Create asynchronous tasks
        :param method: Method to be executed
        :param issue_pr_list: issue or pr list
        :return: task list
        """
        task_list = []
        for issue_or_pr in list(set(issue_pr_list)):
            task_list.append(asyncio.create_task(method(issue_or_pr)))
        return task_list

    async def _get_issue_link_pr(self, issue):
        """
        Get the pr associated with the issue
        :param issue: issue url
        :return: pr url
        """
        pr_list = []
        if Constant.GITHUB in issue:
            pr_list = await self.github_api.issue_relevance_pull(issue_url=issue)
        elif Constant.GITLAB in issue:
            try:
                issue_info = issue.split("/")
                self.gitlab_api.set_attr(owner=issue_info[-5], repo=issue_info[-4])
                self._set_gitlab_host(issue)
                issue_num = issue_info[-1]
                response = await self.gitlab_api.issue_relevance_pull(
                    issue_id=issue_num
                )
                pr_list = self._convert_api_response(response, url_key="web_url")
            except IndexError:
                logger.warning(
                    f"The issue link: {issue} does not conform to the standard format"
                )
        else:
            logger.warning(f"Issue {issue} failed to match the relevant code platform")

        # Record the relationship between issue and pr to facilitate subsequent patch information processing
        logger.info(f"According to issue {issue} get pr: {str(pr_list)}")

        self.issue_pr_dict[issue] = pr_list
        return pr_list

    async def _get_pr_status(self, pr):
        """
        Query pr status
        :param pr: pr
        :return:  self.pr_status_dict
        """
        pr_number = self._set_owner_repo_by_pr(pr)
        if not pr_number:
            logger.info("This is not effective pr:%s" % pr)
            return
        if Constant.GITHUB in pr:
            response = await self.github_api.get_pull_request(pr_number)
            if response:
                self.pr_status_dict[pr] = response.get("state")

        elif Constant.GITLAB in pr:
            self._set_gitlab_host(pr)
            response = await self.gitlab_api.get_single_mr(pr_number)
            if response:
                self.pr_status_dict[pr] = response.get("merge_status")

    async def _get_pr_contain_commits(self, pr):
        """
        Get the commits contained in pr and record them
        :param pr: pull requests
        :return: self.issue_pr_dict
        """
        pr_number = self._set_owner_repo_by_pr(pr)
        if pr_number:
            commits_list = []
            if Constant.GITHUB in pr:
                response = await self.github_api.get_pull_commits(pr_number)
                commits_list = self._convert_api_response(response, "html_url")
            elif Constant.GITLAB in pr:
                self._set_gitlab_host(pr)
                response = await self.gitlab_api.get_mr_context_commits(
                    merge_request_iid=pr_number
                )
                commits_list = self._convert_api_response(response, "web_url")

            logger.info(f"According to pull {pr} get commits: {str(commits_list)}")

            self.issue_pr_dict[pr] = commits_list

    def _set_owner_repo_by_pr(self, pr):
        """
        Set the owner and repo required by the api through the pr link
        :param pr: pull request
        :return: pr number
        """
        pr_info = pr.split("/")
        try:
            pr_number = pr_info[-1]
            if Constant.GITHUB in pr:
                self.github_api.set_attr(owner=pr_info[-4], repo=pr_info[-3])
            elif Constant.GITLAB in pr:
                self.gitlab_api.set_attr(owner=pr_info[-5], repo=pr_info[-4])
            else:
                logger.warning(f"Pr {pr} failed to match the relevant code platform")
                return None
        except IndexError:
            logger.warning(
                f"The pull link: {pr} does not conform to the standard format"
            )
            return None

        return pr_number

    @staticmethod
    def _convert_api_response(response, url_key):
        """
        Process api return result, get pull url or commit url
        :param response: api response
        :param url_key: url key
        :return: pull list or commits list
        """
        pr_commits_list = list()
        if response:
            pr_commits_list.extend(
                [commit_info.get(url_key) for commit_info in response]
            )
        return pr_commits_list

    def _set_gitlab_host(self, issue_pr):
        """
        Set the host of gitlab
        :param issue_pr: issue or pull
        :return: None
        """
        _host_match = re.search(pattern=Constant.GITLAB_HOST_REGEX, string=issue_pr)
        _host = _host_match.group()
        self.gitlab_api.host = _host

    def _set_pr_relation_info(self, pr):
        pr_relation_info = self.pr_relation_info
        pr_relation_info["url"] = pr
        pr_relation_info["commits"] = self.issue_pr_dict.get(pr, [])
        pr_relation_info["status"] = self.pr_status_dict.get(pr)
        return pr_relation_info

    def _convert_path_detail(self):
        """
        Process data and generate patch details
        :return: self.patch_detail_list
        """
        for patch_info in self.patch_info_list:
            patch_detail = self._PatchDetail(
                platform=patch_info.get("platform"), details=list()
            )
            issue_relation_info = self.issue_relation_info

            # First, If there is commits information, pr and issue information will be ignored
            if patch_info.get("commits"):
                pr_relation_info = self.pr_relation_info
                pr_relation_info["commits"] = patch_info.get("commits")
                issue_relation_info["issue"]["prs"].append(pr_relation_info)

            # Second, processing pr information
            for pr in patch_info.get("pr"):
                issue_relation_info["issue"]["prs"].append(
                    self._set_pr_relation_info(pr)
                )

            # None Issue associated patch
            if issue_relation_info["issue"]["prs"]:
                patch_detail.details.append(issue_relation_info)

            # Third, processing issue information
            for issue in patch_info.get("issue"):
                issue_relation_info = self.issue_relation_info
                issue_relation_info["issue"]["url"] = issue
                for pr in self.issue_pr_dict.get(issue, []):
                    issue_relation_info["issue"]["prs"].append(
                        self._set_pr_relation_info(pr)
                    )

                patch_detail.details.append(issue_relation_info)

            self.patch_detail_list.append(dict(patch_detail._asdict()))
